// Copyright (c) 2020 IoTeX Foundation
// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no
// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent
// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache
// License 2.0 that can be found in the LICENSE file.

package action

import (
	"encoding/hex"
	"encoding/json"

	"github.com/pkg/errors"

	"github.com/ethereum/go-ethereum/accounts"

	"github.com/iotexproject/go-pkgs/crypto"
	"github.com/iotexproject/iotex-address/address"
)

const (
	_reclaim = "This is to certify I am transferring the ownership of said bucket to said recipient on IoTeX blockchain"
)

// Errors
var (
	ErrNotSupported = errors.New("signature type not supported")
)

type (
	// a consignment is a transaction signed by transferee which contains an embedded message signed by transferor
	//
	// transferee: the entity/address to receive the ownership of an asset/object
	// transferor: the entity/address to transfer the ownership of the asset/object
	//
	// the transaction contains 2 part:
	// 1. an embedded message that clearly identifies (1) the transferee, (2) the transferor, (3) nonce of the transferee,
	// and (4) a unique ID of the asset/object to be transferred
	// 2. a payload that constitutes a valid ECC signature of the message, by verifying that:
	// (1) the signature is valid
	// (2) signer matches the transferor in the message
	// (3) signer matches the actual owner of the asset/object
	// (4) asset ID in the message matches the ID of asset/object to be transferred on blockchain
	// (5) nonce in the message matches transferee's nonce on blockchain
	//
	// successful verification of above is considered a consent that transferor does own the asset/object and transfer it
	// to transferee, because transferee was able to present such a valid signature (hence the name "consignment"), which
	// can only be generated by transferor

	// Consignment represents a consignment
	Consignment interface {
		Transferor() address.Address
		Transferee() address.Address
		AssetID() uint64
		TransfereeNonce() uint64
	}

	// ConsignMsgEther is the consignment message format of Ethereum
	ConsignMsgEther struct {
		BucketIdx int    `json:"bucket"`
		Nonce     int    `json:"nonce"`
		Recipient string `json:"recipient"`
		Reclaim   string `json:"reclaim"`
	}

	// ConsignJSON is the JSON format of a consignment, it contains the type, message, and signature
	ConsignJSON struct {
		Type string `json:"type"`
		Msg  string `json:"msg"`
		Sig  string `json:"sig"`
	}

	consignment struct {
		index     uint64
		nonce     uint64
		signer    address.Address
		recipient address.Address
	}
)

// NewConsignment creates a consignment from data
func NewConsignment(data []byte) (Consignment, error) {
	c := ConsignJSON{}
	if err := json.Unmarshal(data, &c); err != nil {
		return nil, err
	}

	switch c.Type {
	case "Ethereum":
		return processConsignmentEther(c)
	default:
		return nil, ErrNotSupported
	}
}

func processConsignmentEther(c ConsignJSON) (Consignment, error) {
	// parse embedded msg
	msg := ConsignMsgEther{}
	if err := json.Unmarshal([]byte(c.Msg), &msg); err != nil {
		return nil, err
	}
	if msg.Reclaim != _reclaim {
		return nil, errors.New("reclaim text does not match")
	}

	// verify signature
	sig, err := hex.DecodeString(c.Sig)
	if err != nil {
		return nil, err
	}
	pk, err := RecoverPubkeyFromEccSig(c.Type, []byte(c.Msg), sig)
	if err != nil {
		return nil, err
	}

	con := consignment{}
	con.signer, err = address.FromBytes(pk.Hash())
	if err != nil {
		return nil, err
	}
	con.recipient, err = address.FromString(msg.Recipient)
	if err != nil {
		return nil, err
	}
	con.index = uint64(msg.BucketIdx)
	con.nonce = uint64(msg.Nonce)
	return &con, nil
}

func (c *consignment) Transferor() address.Address {
	return c.signer
}

func (c *consignment) Transferee() address.Address {
	return c.recipient
}

func (c *consignment) AssetID() uint64 {
	return c.index
}

func (c *consignment) TransfereeNonce() uint64 {
	return c.nonce
}

// NewConsignMsg creates a consignment message from inputs
func NewConsignMsg(sigType, recipient string, bucketIdx, nonce uint64) ([]byte, error) {
	switch sigType {
	case "Ethereum":
		msg := ConsignMsgEther{
			BucketIdx: int(bucketIdx),
			Nonce:     int(nonce),
			Recipient: recipient,
			Reclaim:   _reclaim,
		}
		msgBytes, err := json.Marshal(msg)
		if err != nil {
			return nil, err
		}
		return msgBytes, nil
	default:
		return nil, ErrNotSupported
	}
}

// NewConsignJSON creates a consignment JSON from inputs
func NewConsignJSON(sigType, recipient, sig string, bucketIdx, nonce uint64) ([]byte, error) {
	msgBytes, err := NewConsignMsg(sigType, recipient, bucketIdx, nonce)
	if err != nil {
		return nil, err
	}

	msgJSON := ConsignJSON{
		Type: sigType,
		Msg:  string(msgBytes),
		Sig:  sig,
	}
	msgBytes, err = json.Marshal(msgJSON)
	if err != nil {
		return nil, err
	}
	return msgBytes, nil
}

// RecoverPubkeyFromEccSig recovers public key from ECC signature
func RecoverPubkeyFromEccSig(sigType string, msg, sig []byte) (crypto.PublicKey, error) {
	h, err := MsgHash(sigType, msg)
	if err != nil {
		return nil, err
	}
	pk, err := crypto.RecoverPubkey(h, sig)
	if err != nil {
		return nil, err
	}
	if pk.Verify(h, sig) {
		return pk, nil
	}
	return nil, crypto.ErrInvalidKey
}

// MsgHash calculate the hash of msg
func MsgHash(sigType string, msg []byte) ([]byte, error) {
	switch sigType {
	case "Ethereum":
		h, _ := accounts.TextAndHash(msg)
		return h, nil
	default:
		return nil, ErrNotSupported
	}
}
